package p2p

import (
	"bufio"
	"crypto/ecdsa"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum/cmd/utils"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/p2p"
	"github.com/ethereum/go-ethereum/p2p/enode"
	"github.com/ethereum/go-ethereum/p2p/nat"
	"github.com/ethereum/go-ethereum/params"
	whisper "github.com/ethereum/go-ethereum/whisper/whisperv6"
)

const quitCommand = "~Q"

type NameServer interface {
	Query(common.Address, bool) string
}

type NilName string

func (n NilName) Query(addr common.Address, b bool) string {
	return string(n) + ":" + addr.Hex()
}

type Config struct {
	EnodeString string
	// encryption
	AsymKey *ecdsa.PrivateKey
}

type P2PMessager struct {
	asymKey *ecdsa.PrivateKey
	// singletons
	server *p2p.Server
	shh    *whisper.Whisper
	done   chan struct{}
	input  *bufio.Reader
	output *bufio.Writer
	// topics
	topics []Topic
	name   NameServer
}

type Topic struct {
	symKey   []byte
	topic    whisper.TopicType
	filterID string
	passed   string
}

func NewP2PMessager(config *Config, nameServer NameServer) (*P2PMessager, error) {
	ret := new(P2PMessager)

	ret.done = make(chan struct{})
	ret.asymKey = config.AsymKey

	peer := enode.MustParse(config.EnodeString)
	var peers = []*enode.Node{peer}
	for _, mainnode := range params.MainnetBootnodes {
		peer := enode.MustParse(mainnode)
		peers = append(peers, peer)
	}
	whisperConfig := whisper.DefaultConfig
	whisperConfig.RestrictConnectionBetweenLightClients = true
	ret.shh = whisper.New(&whisperConfig)

	asymKeyID, err := ret.shh.AddKeyPair(ret.asymKey)
	if err != nil {
		return nil, fmt.Errorf("Failed to add a key pair: %s", err)
	}

	asymKey, err := ret.shh.GetPrivateKey(asymKeyID)
	if err != nil {
		return nil, fmt.Errorf("Failed to generate a new key pair: %s", err)
	}

	maxPeers := 10
	ret.server = &p2p.Server{
		Config: p2p.Config{
			PrivateKey:     asymKey,
			MaxPeers:       maxPeers,
			Name:           common.MakeName("p2p chat", "6.0"),
			Protocols:      ret.shh.Protocols(),
			NAT:            nat.Any(),
			BootstrapNodes: peers,
			StaticNodes:    peers,
			TrustedNodes:   peers,
		},
	}

	ret.input = bufio.NewReader(os.Stdin)
	ret.output = bufio.NewWriter(os.Stdout)

	ret.name = nameServer

	return ret, nil
}

func (m *P2PMessager) AddToptic(topic, passwd string) error {
	var T Topic

	symKeyID, err := m.shh.AddSymKeyFromPassword(passwd)
	if err != nil {
		return fmt.Errorf("Failed to create symmetric key: %s", err)
	}
	T.symKey, err = m.shh.GetSymKey(symKeyID)
	T.passed = passwd
	copy(T.topic[:], common.FromHex(topic))

	filter := whisper.Filter{
		KeySym:   T.symKey,
		KeyAsym:  nil,
		Topics:   [][]byte{T.topic[:]},
		AllowP2P: true,
	}
	T.filterID, err = m.shh.Subscribe(&filter)
	if err != nil {
		return fmt.Errorf("Failed to install filter: %s", err)
	}

	m.topics = append(m.topics, T)

	return nil
}

func (m *P2PMessager) startServer() error {
	m.server.Start()
	return m.waitForConnection(true)
}

func (m *P2PMessager) waitForConnection(timeout bool) error {
	var cnt int
	var connected bool
	for !connected {
		time.Sleep(time.Millisecond * 500)
		connected = m.server.PeerCount() > 0
		if timeout {
			cnt++
			if cnt > 1000 {
				return fmt.Errorf("Timeout expired, failed to connect")
			}
		}
	}
	return nil
}

func (m *P2PMessager) Run(topic, passwd string) {
	err := m.startServer()
	if err != nil {
		fmt.Println(err)
		return
	}
	defer m.server.Stop()
	err = m.AddToptic(topic, passwd)
	if err != nil {
		fmt.Println(err)
		return
	}
	m.shh.Start(nil)
	finish := make(chan int)
	defer m.shh.Stop()
	fmt.Fprintln(m.output, "Connected to peer,you can type message now.")
	m.output.Flush()
	go m.messageLoop(finish)
	go m.sendLoop()
	<-finish
}

func (m *P2PMessager) sendLoop() {
	for {
		s := m.scanLine(fmt.Sprintf("input %s to quit>", quitCommand))
		if s == quitCommand {
			fmt.Fprintf(m.output, "Quit command received")
			close(m.done)
			break
		}
		m.sendMsg([]byte(s))
	}
}

func (m *P2PMessager) sendMsg(payload []byte) (common.Hash, error) {
	topic := m.topics[0]
	params := whisper.MessageParams{
		Src:      m.asymKey,
		KeySym:   topic.symKey,
		Payload:  payload,
		Topic:    topic.topic,
		TTL:      whisper.DefaultTTL,
		PoW:      whisper.DefaultMinimumPoW,
		WorkTime: 5,
	}

	msg, err := whisper.NewSentMessage(&params)
	if err != nil {
		return common.Hash{}, fmt.Errorf("failed to create new message: %s", err)
	}

	envelope, err := msg.Wrap(&params)
	if err != nil {
		return common.Hash{}, fmt.Errorf("failed to seal message: %v \n", err)
	}

	err = m.shh.Send(envelope)
	if err != nil {
		return common.Hash{}, fmt.Errorf("failed to send message: %v \n", err)
	}

	return envelope.Hash(), nil
}

func (m *P2PMessager) messageLoop(finish chan int) {
	topic := m.topics[0]

	f := m.shh.GetFilter(topic.filterID)
	if f == nil {
		utils.Fatalf("filter is not installed")
	}

	ticker := time.NewTicker(time.Millisecond * 50)

	for {
		select {
		case <-ticker.C:
			messages := f.Retrieve()
			for _, msg := range messages {
				m.printMessageInfo(msg)
			}
		case <-m.done:
			finish <- 0
			return
		}
	}
}

func (m *P2PMessager) printMessageInfo(msg *whisper.ReceivedMessage) {
	text := string(msg.Payload)
	timestamp := time.Unix(int64(msg.Sent), 0).Format("2006-01-02 15:04:05")
	var address common.Address
	if msg.Src != nil {
		address = crypto.PubkeyToAddress(*msg.Src)
	}
	if m.name == nil {
		if whisper.IsPubKeyEqual(msg.Src, &m.asymKey.PublicKey) {
			fmt.Fprintf(m.output, "\n%s <mine>: %s\n", timestamp, text) // message from myself
		} else {
			fmt.Fprintf(m.output, "\n%s [%x]: %s\n", timestamp, address, text) // message from a peer
		}
	} else {
		fmt.Fprintf(m.output, "\n%s [%s]: %s\n", timestamp, m.name.Query(address, false), text)
	}
	m.output.Flush()
}

func (m *P2PMessager) scanLine(prompt string) string {
	if len(prompt) > 0 {
		fmt.Print(prompt)
	}
	txt, err := m.input.ReadString('\n')
	if err != nil {
		utils.Fatalf("input error: %s", err)
	}
	txt = strings.TrimRight(txt, "\n\r")
	return txt
}
